package site.wetsion.galio.sdk.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.GenericFilterBean;
import site.wetsion.galio.sdk.common.*;
import site.wetsion.galio.sdk.handler.*;
import site.wetsion.galio.sdk.strategy.AbstractTokenCheckStrategy;
import site.wetsion.galio.sdk.strategy.OnePerRequestTokenCheckStrategy;
import site.wetsion.galio.sdk.utils.AccessTokenHolderWrapper;
import site.wetsion.galio.sdk.utils.AuthPassowordProvider;
import site.wetsion.galio.sdk.utils.UrlMatchUtil;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
 * rms-sdk 核心过滤器，
 * 提供了在请求外层提供了获取auth认证和校验认证，
 * 成功和失败处理提供了插口，只需自定义实现并实例成bean
 * 获取用户信息也提供了插口，只需自定义实现并实例成bean
 *
 * @author wetsion
 * @version 1.0
 * @CLassName GalioOauthFilter
 * @date 2019/3/8 2:12 PM
 */
@Order(2)
public class GalioOauthFilter extends GenericFilterBean {

    private final static Logger logger = LoggerFactory.getLogger(GalioOauthFilter.class);

    @Autowired
    GalioClientDetailsProperties rmsClientDetailsProperties;

    private AuthenticationFailureHandler authenticationFailureHandler
            = new DefaultAuthenticationFailureHandler();

    private AuthenticationSuccessHandler authenticationSuccessHandler
            = new DefaultAuthenticationSuccessHandler();

    private UserDetailsGetter userDetailsGetter = new DefaultUserDetailsGetter();

    private ClientDetailsGetter clientDetailsGetter = new DefaultClientDetailsGetter();

    private AbstractTokenCheckStrategy tokenCheckStrategy = new OnePerRequestTokenCheckStrategy();

    @Autowired
    OauthClientContext rmsOauthClientContext;

    private UserDetails userDetails;

    private ClientDetails clientDetails;

    // 当前token是否过期，默认不过期
    private boolean currentTokenIsExpire = false;

    @Autowired
    AccessTokenHolderWrapper accessTokenHolderWrapper;

    public GalioOauthFilter() {

    }

    public GalioOauthFilter(AuthenticationFailureHandler authenticationFailureHandler,
                            AuthenticationSuccessHandler authenticationSuccessHandler,
                            UserDetailsGetter userDetailsGetter,
                            ClientDetailsGetter clientDetailsGetter) {
        this.authenticationFailureHandler = authenticationFailureHandler;
        this.authenticationSuccessHandler = authenticationSuccessHandler;
        this.userDetailsGetter = userDetailsGetter;
        this.clientDetailsGetter = clientDetailsGetter;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        logger.debug("entered galio oauth filter");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String currentUri = request.getRequestURI();

        // 查找配置的安全URL，如匹配则直接通过
        for (String sf : this.rmsClientDetailsProperties.getSafetyUrl()) {
            if (UrlMatchUtil.checkMatch(sf, currentUri)) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }
        }
        logger.info("current uri: {}", currentUri);
        try {
            userDetails = userDetailsGetter.handle(request, response);
            clientDetails = clientDetailsGetter.handle(request, response, rmsClientDetailsProperties);
            GalioOauthAccessToken accessToken = handleAccessToken();
            if (Objects.isNull(accessToken)) {
                logger.info("认证失败: token为空");
                failureHandle(request, response);
            } else {
                logger.info("获取token成功，token: {}", accessToken.getAccessToken());
                // 去auth server校验token是否有效
//                JSONObject checkResult = remoteCheckToken(accessToken.getAccessToken());
                tokenCheckStrategy.setRmsClientDetailsProperties(rmsClientDetailsProperties);
                JSONObject checkResult = tokenCheckStrategy.checkToken(
                        Objects.isNull(accessToken.getNext()) ?
                                accessToken.getAccessToken() : accessToken.getNext().getAccessToken());
                if (Objects.nonNull(checkResult)) {
                    if (checkResult.containsKey(GalioOauthConstant.ACTIVE) && checkResult.getBoolean(GalioOauthConstant.ACTIVE)) {
                        // 最终校验成功，active为true
                        // 如果token刷新过，currentTokenIsExpire值将为true，response头增加新token
                        if (this.currentTokenIsExpire) {
                            response.setHeader("x-access-token", accessToken.getAccessToken());
                            response.addHeader("Access-Control-Expose-Headers", "x-access-token");
                            this.currentTokenIsExpire = false;
                        }
                        successHandle(request, response, filterChain);
                    } else {
                        logger.info("token 校验失败，校验结果: {}", checkResult.toJSONString());
                        rmsOauthClientContext.setAccessToken(null);
                        failureHandle(request, response);
                    }
                } else {
                    logger.info("token 校验异常，校验结果为空");
                    failureHandle(request, response);
                }
            }
        } catch (Exception e) {
            logger.info("获取认证失败: {}", e.getMessage());
            failureHandle(request, response);
        }
    }

    private void successHandle(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        authenticationSuccessHandler.onAuthenticationSuccess(request, response, filterChain);
    }

    private void failureHandle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        authenticationFailureHandler.onAuthenticationFailure(request, response);
    }

    /**
     * 先从本地上下文获取token，进行判断
     **/
    private GalioOauthAccessToken handleAccessToken() throws Exception {
        // 先从上下文中获取accessToken
        GalioOauthAccessToken rmsOauthAccessToken = rmsOauthClientContext.getAccessToken();
        // token 接近过期时
        if (Objects.nonNull(rmsOauthAccessToken) && (rmsOauthAccessToken.nearExpired() || rmsOauthAccessToken.isExpired())) {
            this.currentTokenIsExpire = true;
            try {
                AccessTokenRequest rmsAccessTokenRequest = rmsOauthClientContext.getAccessTokenRequest();
                if (Objects.isNull(rmsAccessTokenRequest)) {
                    throw new Exception("No OAuth 2 security context has been established. Unable to access rms");
                }
                String stateKey = rmsAccessTokenRequest.getStateKey();
                if (stateKey != null) {
                    rmsAccessTokenRequest.setPreservedState(rmsOauthClientContext.removePreservedState(stateKey));
                }

                GalioOauthAccessToken existingToken = rmsOauthClientContext.getAccessToken();
                if (existingToken != null) {
                    rmsAccessTokenRequest.setExistingToken(existingToken);
                }

                // 获取用户信息
                UserDetails beforeUserDetails = Objects.nonNull(this.userDetails) ?
                        this.userDetails : (
                        Objects.nonNull(rmsOauthAccessToken.getUserDetails()) ?
                                rmsOauthAccessToken.getUserDetails() : accessTokenHolderWrapper.getTokenUser(rmsOauthAccessToken.getAccessToken())
                );
                logger.info("刷新前获取用户信息：[{}]", JSON.toJSONString(beforeUserDetails));

                // 刷新token
                GalioOauthAccessToken refreshAccessToken = obtainRefreshToken(rmsOauthAccessToken.getRefreshToken());

                if (Objects.nonNull(refreshAccessToken)) {
                    logger.info("刷新token，刷新前token：[{}]，刷新后token：[{}]",
                            rmsOauthAccessToken.getAccessToken(), refreshAccessToken.getAccessToken());
                    // 如果刷新的token不为空，将之前的token缓存删除，保存新的token
                    accessTokenHolderWrapper.remove(rmsOauthAccessToken.getAccessToken());

                    refreshAccessToken.setPre(rmsOauthAccessToken);
                    refreshAccessToken.setUserDetails(beforeUserDetails);
                    accessTokenHolderWrapper.save(refreshAccessToken.getAccessToken(), refreshAccessToken);

                    // 下面两行将刷新后的token设置到之前token的next中，再将之前token重新写入redis，
                    // 此处目的是为了兼容老版本前后端未分离，无法通过header拿到新的token的情况，浏览器始终持有第一次的token，
                    // 所以需要之前的token始终生效，通过刷新的token去维持旧token的生命
                    rmsOauthAccessToken.setNext(refreshAccessToken);
                    rmsOauthAccessToken.setUserDetails(beforeUserDetails);
                    accessTokenHolderWrapper.save(rmsOauthAccessToken.getAccessToken(), rmsOauthAccessToken);

                    // 将之前的token 用户对应关系缓存删除，保存新的token用户对应关系
                    UserDetails preUserDetails = accessTokenHolderWrapper.removeTokenUser(rmsOauthAccessToken.getAccessToken());

                    if (Objects.isNull(preUserDetails)) {
                        preUserDetails = beforeUserDetails;
                    }
                    logger.debug("刷新后获取用户信息：[{}]", JSON.toJSONString(preUserDetails));

                    accessTokenHolderWrapper.saveUser(refreshAccessToken, preUserDetails);
                    accessTokenHolderWrapper.saveUser(rmsOauthAccessToken, preUserDetails);
                }

                rmsOauthAccessToken = refreshAccessToken;
                if (rmsOauthAccessToken == null || rmsOauthAccessToken.getValue() == null) {
                    throw new IllegalStateException(
                            "Access token provider returned a null access token, which is illegal according to the contract.");
                }
                rmsOauthClientContext.setAccessToken(rmsOauthAccessToken);
            } catch (Exception e) {
                throw e;
            }
        }
        // 如果当前上下文中没有token，则去远程auth尝试再获取一次，
        // 5月20更新，如果上下文无token，说明请求头无x-access-token，则视为认证失败，不再尝试再获取
        // if (Objects.isNull(rmsOauthAccessToken)) {
        //    rmsOauthAccessToken = obtainAccessToken();
        // }
        return rmsOauthAccessToken;
    }

    /**
     * 从远程auth刷新token
     *
     * @Param [refreshToken] 用于刷新的token
     * @return com.dxy.data.rms.sdk.common.RmsOauthAccessToken
     **/
    private GalioOauthAccessToken obtainRefreshToken(String refreshToken) {
        return AuthPassowordProvider.refreshToken(rmsClientDetailsProperties.getAccessTokenUrl(),
                refreshToken, clientDetails.getClientId(), clientDetails.getClientSecret());
    }

    /**
     * 从远程auth server获取token认证
     * @return com.wetsion.securityoauthdemo.rmsclient.common.RmsOauthAccessToken
     **/
    private GalioOauthAccessToken obtainAccessToken() {
        return AuthPassowordProvider.handle(rmsClientDetailsProperties.getAccessTokenUrl(),
                userDetails.getUsername(), userDetails.getPassword(), clientDetails.getClientId(),
                clientDetails.getClientSecret());
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
        this.authenticationSuccessHandler = authenticationSuccessHandler;
    }

    public void setClientDetailsGetter(ClientDetailsGetter clientDetailsGetter) {
        this.clientDetailsGetter = clientDetailsGetter;
    }

    public void setUserDetailsGetter(UserDetailsGetter userDetailsGetter) {
        this.userDetailsGetter = userDetailsGetter;
    }

    public void setTokenCheckStrategy(AbstractTokenCheckStrategy tokenCheckStrategy) {
        this.tokenCheckStrategy = tokenCheckStrategy;
    }
}
